﻿using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Drawing;
using System.IO;
using System.IO.Compression;
using System.Linq;
using System.Net;
using System.Net.Cache;
using System.Net.Security;
using System.Security.Cryptography.X509Certificates;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using Newtonsoft.Json;

namespace MX.Core.Http.WebAction
{

    public abstract class WebAction
    {
        /* 类名:web操作基础支持类
         * 描述:提供web操作基础接口
         * 创建日期:2011-10-25
         * 版本:0.4
         * 作者:by rolends986
         */
        /*
         * 版本更新记录
         * 0.1 基本代码的构建与调试 
         * 0.2 修改主入口方法,实现多参数化定义 2011-11-1
         * 0.3 添加SetUseUnsafeHeaderParsing功能,修订lock逻辑,删除url编码逻辑(会导致部分服务器header解析问题) 2011-12-2
         * 0.4 新增代理控制逻辑,修改useUnsafeHeaderParsing参数,添加资源释放逻辑 2011-12-12
         */

        static WebAction()
        {
            DefaultConnectionLimit = 100000;
            KeepAliveTime = 10 * 1000;
            KeepAliveInterval = 300;
            //   XQException.InitErrorCodeFile(Path.Combine(Path.GetDirectoryName(Assembly.GetCallingAssembly().CodeBase).Replace("file:\\", ""), "shishicai.ert"));
        }

        protected CookieManager _cookieManager = new CookieManager();


        public WebAction()
        {

        }

        public const string _userAgent_FF = "Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/53.0.2785.116 Safari/537.36";

        public const string _userAgent_IE =
            "Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1; Trident/4.0; .NET CLR 2.0.50727; .NET CLR 3.0.4506.2152; .NET CLR 3.5.30729; .NET4.0C; .NET4.0E)";

        public const string _defaultAccept = "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8";
        public const string _defaultAcceptLanguage = "zh-cn,zh;q=0.5";
        public const string _defaultAcceptCharset = "GB2312,utf-8;q=0.7,*;q=0.7";

        public static int DefaultConnectionLimit { get; set; }

        public static int KeepAliveTime { get; set; }

        public static int KeepAliveInterval { get; set; }

        public bool EnableProxy { get; set; }

        public string Key
        {
            get { return new Guid().ToString(); }
        }

        /// <summary>
        /// ssl安全回调
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="certificate"></param>
        /// <param name="chain"></param>
        /// <param name="errors"></param>
        /// <returns></returns>
        public static bool CheckValidationResult(object sender, X509Certificate certificate, X509Chain chain,
                                                  SslPolicyErrors errors)
        {
            //if (sender is HttpWebRequest)
            //{
            //    var request = sender as HttpWebRequest;
            //    if (request.ClientCertificates.Count > 0)
            //        return false;
            //}
            return true; // Always accept
        }



        /// <summary>
        ///   新方法 by rolends1986 
        ///   * 2011-10-27
        ///  1,支持自定义Content-Type
        ///  2,封装编码,自身维护表单键值转换
        ///  3,支持各种参数的自定义
        ///  4,实现自动编码
        ///  5,实现CA文件指定
        /// </summary>
        /// <typeparam name="T">返回类型，string【文本】，ResponseLocation【包含跳转的文本】， Image【图片】，byte[]【二进制】，其他类型【使用文本反序列化为对象】</typeparam>
        /// <param name="url">包含http的URLurl字符</param>
        /// <param name="charset">编码</param>
        /// <param name="referer">referer头</param>
        /// <param name="postInfo">post信息</param>
        /// <param name="useProxy">是否使用代理</param>
        /// <param name="headers">自定义http头</param>
        /// <param name="userAgent">userAgent</param>
        /// <param name="protocol">http协议版本</param>
        /// <param name="allowAutoRedirect">是否允许重定向</param>
        /// <param name="keepAlive">是否允许keepAlive</param>
        /// <param name="accept">http头accept值</param>
        /// <param name="acceptLanguage">http头acceptLanguage值</param>
        /// <param name="acceptCharset">http头acceptCharset值</param>
        /// <param name="urlEncoding">http头urlEncoding值</param>
        /// <param name="cachePolicy">http头cachePolicy值</param>
        /// <param name="timeOut">http请求超时时间，单位毫秒</param>
        /// <returns>请求结果：string【文本】，ResponseLocation【包含跳转的文本】，Image【图片】，byte[]【二进制】，其他类型【使用文本反序列化为对象】</returns>
        public virtual T GetWebData<T>(string url,
                                           string charset = null,
                                           string referer = null,
                                           PostInfo postInfo = null,
                                           bool? useProxy = null,
                                           NameValueCollection headers = null,
                                           string userAgent = null,
                                           Version protocol = null,
                                           bool? allowAutoRedirect = false,
                                           bool? keepAlive = null,
                                           string accept = null,
                                           string acceptLanguage = null,
                                           string acceptCharset = null,
                                           string urlEncoding = null,
                                           RequestCachePolicy cachePolicy = null,
                                           int timeOut = 60 * 1000)
        {
            System.Net.ServicePointManager.DefaultConnectionLimit = DefaultConnectionLimit;
            //System.Net.ServicePointManager.SetTcpKeepAlive(true, KeepAliveTime, KeepAliveInterval);
            //SetUseUnsafeHeaderParsing(useUnsafeHeaderParsing);

            var uri = new Uri(url);
            //url = EncodeUrl(url, urlEncoding);
            var request = (HttpWebRequest)WebRequest.Create(url);
            request.ServicePoint.Expect100Continue = false;
            request.ServicePoint.ConnectionLimit = DefaultConnectionLimit;
            request.Proxy = null;
            request.Timeout = timeOut;


            #region set default

            request.KeepAlive = false;
            request.AllowAutoRedirect = false;
            request.UserAgent = _userAgent_FF;
            request.Accept = _defaultAccept;
            request.Headers.Add(HttpRequestHeader.AcceptLanguage, _defaultAcceptLanguage);
            request.Headers.Add(HttpRequestHeader.AcceptCharset, _defaultAcceptCharset);
            request.CachePolicy =
                new System.Net.Cache.RequestCachePolicy(System.Net.Cache.RequestCacheLevel.NoCacheNoStore);
            request.Method = "get";
            request.ProtocolVersion = HttpVersion.Version11;
            #endregion

            //request.Host = uri.Host;
            if (allowAutoRedirect.HasValue)
                request.AllowAutoRedirect = allowAutoRedirect.Value;
            if (keepAlive.HasValue) request.KeepAlive = keepAlive.Value;//由于手动释放了资源,keepalive设置不再有效
            if (!string.IsNullOrEmpty(userAgent))
                request.UserAgent = userAgent;
            if (!string.IsNullOrEmpty(accept))
                request.Accept = accept;
            if (!string.IsNullOrEmpty(acceptLanguage))
            {
                if (request.Headers[HttpRequestHeader.AcceptLanguage] == null)
                    request.Headers.Add(HttpRequestHeader.AcceptLanguage, acceptLanguage);
                else
                    request.Headers[HttpRequestHeader.AcceptLanguage] = acceptLanguage;
            }
            if (!string.IsNullOrEmpty(acceptCharset))
            {
                if (request.Headers[HttpRequestHeader.AcceptCharset] == null)
                    request.Headers.Add(HttpRequestHeader.AcceptCharset, acceptCharset);
                else
                    request.Headers[HttpRequestHeader.AcceptCharset] = acceptCharset;
            }
            if (!string.IsNullOrEmpty(referer))
                request.Referer = referer;
            if (cachePolicy != null)
                request.CachePolicy = cachePolicy;
            if (protocol != null)
                request.ProtocolVersion = protocol;
            try
            {
                if (headers != null)
                    foreach (var nv in headers.AllKeys)
                    {
                        request.Headers.Add(nv, headers[nv]);
                    }
            }
            catch (Exception ex)
            {
                DisposeRequest(request);
                //request header error 502
                ThrowException(502, ex.Message, ex);
            }
            string requestCookie = _cookieManager.GetCookieHeader(uri);
            if (!String.IsNullOrEmpty(requestCookie))
            {
                request.Headers.Add(HttpRequestHeader.Cookie, requestCookie);
            }
            if (postInfo != null)
            {
                request.Method = "post";
                byte[] byteArray = postInfo.GetPostData();
                request.ContentType = postInfo.GetContentType();
                request.ContentLength = byteArray.Length;
                Stream dataStream = request.GetRequestStream();
                dataStream.Write(byteArray, 0, byteArray.Length);
                dataStream.Close();
            }
            WebResponse response = null;
            try
            {
                response = request.GetResponse();
            }
            catch (Exception ex)
            {
                DisposeRequest(request);
                DisposeResponse(response);
                //get response error 503
                ThrowException(503, ex.Message, ex);
            }
            string cookie = response.Headers.Get("Set-Cookie");
            if (!String.IsNullOrEmpty(cookie))
            {
                _cookieManager.SetCookie(cookie, uri);
            }
            var sm = response.GetResponseStream();
            if (typeof(T) == typeof(string))
            {
                if (string.IsNullOrEmpty(charset))
                {
                    if (!String.IsNullOrEmpty(response.Headers["Content-Type"]))
                    {
                        string[] ct = response.Headers["Content-Type"].Split(';');
                        if (ct.Length > 1)
                        {
                            charset = ct[1].Split('=')[1]; //set server response encoding
                        }
                    }
                }
                string contentEncoding = response.Headers["Content-Encoding"];
                string html = GetHtml(sm, contentEncoding, charset);
                T result = (T)(object)html;
                DisposeRequest(request);
                DisposeResponse(response);
                return result;
            }
            else if (typeof(Image) == typeof(T))
            {
                try
                {
                    Image original = Image.FromStream(sm);
                    T result = (T)(object)original;
                    DisposeRequest(request);
                    DisposeResponse(response);
                    return result;
                }
                catch (Exception ex)
                {
                    //to image error 504
                    DisposeRequest(request);
                    DisposeResponse(response);
                    ThrowException(504, ex.Message, ex);
                    return default(T);
                }

            }
            else if (typeof(ResponseLocation) == typeof(T))
            {
                ResponseLocation rl = new ResponseLocation()
                {
                    Html = GetHtml(sm, "", charset),
                    Url = response.Headers["Location"]
                };
                T result = (T)(object)rl;
                DisposeRequest(request);
                DisposeResponse(response);
                return result;

            }
            else if (typeof(byte[]) == typeof(T))
            {
                T result = (T)(object)GetData(sm);
                DisposeRequest(request);
                DisposeResponse(response);
                return result;
            }
            else
            {
                try
                {
                    return JsonConvert.DeserializeObject<T>(GetHtml(sm, "", charset));
                }
                catch (Exception ex)
                {
                    //to image error 504
                    DisposeRequest(request);
                    DisposeResponse(response);
                    ThrowException(504, ex.Message, ex);
                    return default(T);
                }
            }
        }

        /// <summary>
        /// 释放response
        /// </summary>
        /// <param name="response"></param>
        private void DisposeResponse(WebResponse response)
        {
            try
            {  
                response.GetResponseStream().Close();
            }
            catch
            {
            }
            try
            {
                response.Close();
            }
            catch
            {
            }
            try
            {
                response = null;
            }
            catch
            {
            }
        }

        /// <summary>
        /// 释放request
        /// </summary>
        /// <param name="request"></param>
        private void DisposeRequest(HttpWebRequest request)
        {
            try
            {
                try
                { 
                    request.GetRequestStream().Close();
                }
                catch
                {
                }
                try
                {
                    request.Abort();
                }
                catch
                {
                }
                try
                {
                    request = null;
                }
                catch
                {
                }
            }
            catch
            {
            }
        }



        public byte[] GZipDecompress(byte[] gzip)
        {
            using (GZipStream stream = new GZipStream(new MemoryStream(gzip),
                                           CompressionMode.Decompress))
            {
                const int size = 4096;
                byte[] buffer = new byte[size];
                using (MemoryStream memory = new MemoryStream())
                {
                    int count = 0;
                    do
                    {
                        count = stream.Read(buffer, 0, size);
                        if (count > 0)
                        {
                            memory.Write(buffer, 0, count);
                        }
                    } while (count > 0);
                    return memory.ToArray();
                }
            }
        }

        public byte[] DeflateDecompress(byte[] deflate)
        {
            using (DeflateStream stream = new DeflateStream(new MemoryStream(deflate),
                                              CompressionMode.Decompress))
            {
                const int size = 4096;
                byte[] buffer = new byte[size];
                using (MemoryStream memory = new MemoryStream())
                {
                    int count = 0;
                    do
                    {
                        count = stream.Read(buffer, 0, size);
                        if (count > 0)
                        {
                            memory.Write(buffer, 0, count);
                        }
                    } while (count > 0);
                    return memory.ToArray();
                }
            }
        }

        private byte[] GetData(Stream sm)
        {
            byte[] realData = null;
            byte[] buffer = new byte[1024 * 8];
            int dataLength = 0;
            do
            {
                dataLength = sm.Read(buffer, 0, buffer.Length);
                if (realData == null)
                {
                    realData = new byte[dataLength];
                    Array.Copy(buffer, realData, dataLength);
                }
                else
                {
                    int oldLength = realData.Length;
                    Array.Resize<byte>(ref realData, realData.Length + dataLength);
                    Array.Copy(buffer, 0, realData, oldLength, dataLength);
                }


            } while (dataLength > 0);
            //return (T)(object)buffer.Take(dataLength).ToArray();
            return realData;
        }

        private string GetHtml(Stream sm, string contentEncoding, string charset)
        {
            var data = GetData(sm);
            if ("gzip".Equals(contentEncoding, StringComparison.InvariantCultureIgnoreCase))
            {
                data = GZipDecompress(data);
            }
            string newCharset = string.IsNullOrEmpty(charset) ? "utf-8" : charset;
            try
            {
                string r = Encoding.GetEncoding(newCharset).GetString(data);
                if (string.IsNullOrEmpty(charset))
                {
                    r = CheckEncoding(data, newCharset, r);
                }
                //LogObject.WriteLine("==============================================\r\n");
                //LogObject.WriteLine(r);
                //LogObject.WriteLine("==============================================");
                //LogObject.WriteLine("******************************分割*************************");
                return r;
            }
            catch (Exception ex)
            {
                //get response error 503
                ThrowException(509, ex.Message, ex);
                return "";
            }
        }

        protected static Regex regCharset = new Regex("(?<=<meta.+?content\\=.+?charset\\=).+?(?=(\\\"|[\\s]))",
                                                RegexOptions.IgnoreCase);

        protected static Regex regCharset2 = new Regex("(?<=<meta[\\s]+charset=[\\\"]{0,1})[a-z0-9]+(?=[\\\"]{0,1})",
                                                 RegexOptions.IgnoreCase);

        private string CheckEncoding(byte[] data, string currentCharset, string html)
        {
            string pageCharset = "";
            if (regCharset.IsMatch(html))
            {
                pageCharset = regCharset.Match(html).Value.Trim().ToLower();
            }
            if (regCharset2.IsMatch(html))
            {
                pageCharset = regCharset2.Match(html).Value.Trim().ToLower();
            }
            if (pageCharset != currentCharset.Trim().ToLower())
            {
                try
                {
                    return Encoding.GetEncoding(pageCharset).GetString(data);
                }
                catch
                {
                }
            }
            return html;
        }

        protected virtual X509CertificateCollection GetCertificates(Uri uri)
        {
            X509CertificateCollection certs = new X509CertificateCollection();
            string host = uri.Host;
            for (int i = 0; i < 8; i++)
            {
                for (int j = 1; j <= 2; j++)
                {
                    X509Store store = new X509Store((StoreName)(i + 1), (StoreLocation)j);
                    store.Open(OpenFlags.MaxAllowed);
                    foreach (var cert in store.Certificates)
                    {
                        Console.WriteLine(cert.Subject);
                        if (cert.Subject.ToLower().Contains(host.ToLower()))
                        {
                            certs.Add(cert);
                        }
                    }

                }

            }
            return certs;
        }

        protected virtual void ThrowException(int errorCode, string sourceText, Exception ex)
        {
            var exception = new Exception(errorCode + ":" + sourceText, ex);
            throw exception;
        }

        protected NameValueCollection GetQueryParameter(string url)
        {
            NameValueCollection pars = new NameValueCollection();
            var paraString = url.Substring(url.IndexOf("?") + 1, url.Length - url.IndexOf("?") - 1).Split('&');
            for (int i = 0; i < paraString.Length; i++)
            {
                if (!paraString[i].Contains("="))
                    continue;
                var nv = paraString[i].Split('=');
                var name = nv[0];
                var value = nv[1];
                pars.Add(name, value);
            }
            return pars;
        }
    }
}
